package kryptoBlatt2;

import java.util.Arrays;

//Lineares Schieberegister
public class LSR {

	private int[] startZustand;
	private int[] currentState;
	private IFunction<Integer, Integer> funktion;
	private String outputHistory;
	private String currentOutput;
	private int periode;
	private int steps;

	public LSR(int[] startZustand, IFunction<Integer, Integer> funktion) {
		initVars(startZustand, funktion);
	}

	public LSR() {
		this.periode = 0;
		this.outputHistory = "";
		this.currentOutput = "0";
		this.steps = 0;
	}

	//Veriablieb initialisieren
	private void initVars(int[] startZustand, IFunction<Integer, Integer> funktion){
		this.startZustand = startZustand;
		this.currentState = startZustand;
		this.funktion = funktion;
		this.periode = 0;
		this.outputHistory = "";
		this.currentOutput = "0";
		this.steps = 0;
	}

	//Periode der Outputfolge berechnen
	private void calcPeriode(){
		int[] currentState = startZustand.clone();
		for (int i = 1; this.periode == 0; i++) {
			currentState = shiftLSR(currentState, funktion);
			if(checkIfPeriode()){
				this.periode = i;
			}
		}	
	}

	//Abfrage ob der aktuelle Zustand dem Startzustand entspricht, falls ja : Periode erreicht
	public boolean checkIfPeriode(){
		return (this.steps > 0) && (Arrays.equals(currentState, startZustand)); 
	}

	//Das LSR um eine belibige Anzahl von Schritten verschieben, Anzahl der Schritte = steps
	public int[] shift(int steps){
		int[] acc = currentState.clone();
		for (int i = 0; i < steps; i++) {
			acc = shiftLSR(acc, funktion);
		}
		return acc;
	}

	//LSR um einen Schritt verschieben gemaess seiner Verschiebefunktion
	private int[] shiftLSR(int[] currentState, IFunction<Integer, Integer> funktion) {
		int[] acc = currentState.clone();
		acc = turnArray(acc);
		Integer[] temp = intToIntegerArray(acc);
		int value = (Integer) funktion.apply(temp);
		acc = turnArray(acc);
		this.outputHistory += acc[0];
		this.currentOutput = acc[0] + "";
		acc = shiftArray(acc);
		acc[acc.length - 1] = value;
		this.currentState = acc;
		this.steps++;
		return acc;
	}

	//int[] Array in Integer[] array konvertieren, fuer IFunction Interface
	private Integer[] intToIntegerArray(int[] array){
		int length = array.length;
		Integer[] acc = new Integer[length];
		for (int i = 0; i < length; i++) {
			acc[i] = array[i];
		}
		return acc;
	}

	//Array um eine Stelle verschieben, dabei gilt: letzter Wert = erster wert
	private int[] shiftArray(int[] array){
		int[] acc = array.clone();
		int length = acc.length;
		int val = acc[0];
		for (int i = 0; i < length - 1; i++) {
			acc[i] = acc[i + 1];
		}
		acc[length - 1] = val;
		return acc;
	}

	//Aktueller Zustand des LSR's ausgeben
	public void printCurrentState(){
		System.out.println("Aktueller Zustand" + Arrays.toString(getCurrentState()));
	}

	//Aktueller Zustand des LSR's ausgeben
	public void printBeginState(){
		System.out.println("Startzustand: " + Arrays.toString(getStartZustand()));
	}

	//Startzustand ausgeben
	public int[] getStartZustand() {
		return startZustand;
	}

	//Aktuellen Zustand des LSR's abfragen
	private int[] getCurrentState(){
		return turnArray(currentState);
	}

	//Periode der Outputfolge berechnen und ausgeben
	public int getPeriode() {
		calcPeriode();
		return periode;
	}

	//Output des aktuellen Zustandes ausgeben
	public String getCurrentOutput() {
		return currentOutput;
	}

	//Bisherige Outputhistory ausgeben, also diejenige Ausgabefolge bis zum aktuellen Zustand
	public String getOutputHistory() {
		return outputHistory;
	}

	//Funktion zum exponieren
	private int exp(int x, int n){
		int acc = 1;
		for (int i = 0; i < n; i++) {
			acc *= x;
		}
		return acc;
	}

	//Alle moeglichen Startzustaende des LSR's durchprobieren mit der Verschiebefunktion des LSR's zum testen ob sich die Periode aendert
	public String checkBeginValues(){
		calcPeriode();
		String acc = "";
		int[] state;
		String acc2 = "Anfangszustand: [";
		int length = startZustand.length;
		for (int i = 0; i < length - 1; i++) {
			acc2 += "a" + i + ", ";
		}
		acc2 += "a" + (length - 1)  + "]\n";
		for (int i = 0; i < exp(2, startZustand.length); i++) {
			state = turnArray(intToBinArray(i));
			LSR lsr = new LSR(state, funktion);
			int periode = lsr.getPeriode();
			if(periode != this.periode){
				acc += "Anfangszustand: " + Arrays.toString(state) + ", Periodenlaenge: " + periode + "\n"; 
			}
		}
		return acc2 + acc;
	}

	//Dezimalzahl in Binaerzahl umwandeln
	private int[] intToBinArray(int i) {
		int length = startZustand.length;
		int[] acc = new int[length]; 
		int temp = 0;
		for (int j = 0; j < length; j++) {
			temp = length - j - 1;
			if(exp(2, temp) > i){
				acc[temp] = 0;
			} 
			else{
				acc[length - 1 - j] = 1;
				i -= exp(2, temp);
			}
		}
		return acc;
	}

	//Array spiegeln
	private int[] turnArray(int[] array){
		int length = array.length;
		int[] acc = new int[length];
		for (int i = 0; i < length; i++) {
			acc[i] = array[(length - 1) - i];
		}
		return acc;
	}

	//Zahlenfolge in Form eines Strings, als Ganzzahl Array zurueckgeben
	private int[] getDigits(String folge){
		int length = folge.length();
		int[] acc = new int[length];
		for (int i = 0; i < length; i++) {
			acc[i] = Integer.parseInt(folge.charAt(i) + "");
		}
		return acc;
	}

	//Bestimmung der Verschiebefunktion und des Startzustandes bei gegebener Ausgabefolge (Aufgabe 4)
	public void getLSR(String folge){
		int[] vals = getDigits(folge);
		int[] beginState = getHalf(vals, 0);
		int n = vals.length/2;
		BinMatrix m1 = new BinMatrix(n, n);
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++) {
				m1.setValueAt(i, j, vals[i + j]);
			}
		}
		//		System.out.println("Matrix A nach Skript:");
		//		m1.printIntegerMatrix();
		try {
			m1 = m1.inverse(m1);
		} catch (NoSquareException e) {
			e.printStackTrace();
		} catch (DeterminanteZeroException e) {
			e.printStackTrace();
		}
		//		System.out.println("Inverse Matrix A^-1:");
		//		m1.printIntegerMatrix();
		int[] upperHalf = getHalf(vals, 1);
		BinMatrix m2 = new BinMatrix(upperHalf);
		//		System.out.println("Vektor von n bis 2n-1:");
		//		m2.printIntegerMatrix();
		BinMatrix m3 = m1.matrixMult(m2);
		IFunction<Integer, Integer> funk = new Function(turnArray(getArrayColumn(m3.getMatrix(), 0)));
		//		funk.printCoeffs();
		this.startZustand = beginState;
		this.currentState = beginState;
		this.funktion = funk;
	}

	//Einzelne Spalte eines zweidimensionales Arrays ausgeben
	private int[] getArrayColumn(int[][] array, int col){
		int length = array.length;
		int[] acc = new int[length];
		for (int i = 0; i < length; i++) {
			acc[i] = (int) array[i][col];
		}
		return acc;
	}

	//die haelfte eines Arrays ausgeben, upper bestimmt ob untere oder obere haelfte
	private int[] getHalf(int[] vals, int upper){
		int half = (int) Math.floor(vals.length / 2);
		int[] acc = new int[half];
		int offset = upper * half;
		for (int i = 0 + offset, j = 0; i < half + offset; i++,j++) {
			acc[j] = vals[i];
		}
		return acc;
	}

	//Aktuellen Zustand des LSR's setzen
	public void setCurrentState(int[] currentState) {
		this.currentState = currentState;
	}

	//Aktuellen Output des LSR's setzen
	public void setCurrentOutput(String currentOutput) {
		this.currentOutput = currentOutput;
	}

	//Komplette Output history setzen
	public void setOutputHistory(String outputHistory) {
		this.outputHistory = outputHistory;
	}

	//Periode setzen
	public void setPeriode(int periode) {
		this.periode = periode;
	}

	//LSR's klonen
	public LSR clone(){
		int[] beginState = startZustand.clone();
		int[] currState = currentState.clone();
		IFunction<Integer, Integer> funk = this.funktion.clone();
		String outputHistory = this.outputHistory + "";
		String currentOutput = this.currentOutput + "";
		int periode = this.periode;
		LSR lsr = new LSR(beginState, funk);
		lsr.setCurrentOutput(currentOutput);
		lsr.setCurrentState(currState);
		lsr.setOutputHistory(outputHistory);
		lsr.setPeriode(periode);
		return lsr;
	}

	//LSR auf Startzustand zuruecksetzen
	public void resetLSR(){
		initVars(this.startZustand, this.funktion);
	}

	//Koeffizienten der Verschiebefunktion ausgeben
	public void printFunctionCoeffs(){
		this.funktion.printCoeffs();
	}

	//Ausgabefolge ausgeben
	public void printOutputHistory(){
		System.out.println(getOutputHistory() + "\n");
	}

}
